﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.IO;
using System.Text;
using System.Xml;

namespace Argos.Diagnostics
{
	/// <summary>
	/// A trace listener that writes traces to a file so that each
	/// event is written on a single line where different fields are
	/// delimited by tabs.
	/// </summary>
	public class RollingTabDelimitedTraceListener : TraceListener
	{

		private RollingTabDelimitedTraceListener() { }

		#region #ctor: public RollingTabDelimitedTraceListener(string outputFolder)

		/// <summary>
		/// Creates a new instance of the class and specifies the path
		/// to the folder where the trace listener will store traced data.
		/// </summary>
		/// <param name="outputFolder">
		/// The full path to the folder where the trace listener should
		/// write the traced data.
		/// </param>
		public RollingTabDelimitedTraceListener(string outputFolder)
		{
			if(!Directory.Exists(outputFolder))
			{
				throw new ArgumentException("The given output folder does not exist.");
			}
			this.OutputFolder = outputFolder;
		}

		#endregion


		private static object StreamLock = new object();

		private string OutputFolder { get; set; }



		#region public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data)

		/// <summary>
		/// Writes the given data to the underlying text file.
		/// </summary>
		/// <param name="eventCache">Detailed information about the event.</param>
		/// <param name="source">The source name of the event.</param>
		/// <param name="eventType">The type of the event.</param>
		/// <param name="id">The id of the event.</param>
		/// <param name="data">The data to write.</param>
		public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data)
		{
			if (null != this.Filter && !this.Filter.ShouldTrace(eventCache, source, eventType, id, null, null, data, null))
				return;

			this.TraceData(eventCache, source, eventType, id, new object[] { data });
		}

		#endregion

		#region public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data)

		/// <summary>
		/// Writes the given data to the underlying text file.
		/// </summary>
		/// <param name="eventCache">Detailed information about the event.</param>
		/// <param name="source">The source name of the event.</param>
		/// <param name="eventType">The type of the event.</param>
		/// <param name="id">The id of the event.</param>
		/// <param name="data">An array of data to write.</param>
		public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data)
		{

			if (null != this.Filter && !this.Filter.ShouldTrace(eventCache, source, eventType, id, null, null, null, data))
				return;

			this.WriteLine(eventCache, source, eventType, id, data);
		}

		#endregion

		#region public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id)

		/// <summary>
		/// Writes an event to the underlying text file.
		/// </summary>
		/// <param name="eventCache">Detailed information about the event.</param>
		/// <param name="source">The source name of the event.</param>
		/// <param name="eventType">The type of the event.</param>
		/// <param name="id">The id of the event.</param>
		public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id)
		{
			if (null != this.Filter && !this.Filter.ShouldTrace(eventCache, source, eventType, id, null, null, null, null))
				return;

			this.TraceData(eventCache, source, eventType, id, new object[0]);
		}

		#endregion

		#region public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args)

		/// <summary>
		/// Writes a formatted message to the underlying text file.
		/// </summary>
		/// <param name="eventCache">Detailed information about the event.</param>
		/// <param name="source">The source name of the event.</param>
		/// <param name="eventType">The type of the event.</param>
		/// <param name="id">The id of the event.</param>
		/// <param name="format">A string specifying the format string for the arguments given in <paramref name="args"/>.</param>
		/// <param name="args">The arguments to format as specified in <paramref name="format"/>.</param>
		public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args)
		{
			if (null != this.Filter && !this.Filter.ShouldTrace(eventCache, source, eventType, id, format, args, null, null))
				return;

			this.TraceData(eventCache, source, eventType, id, new object[] { string.Format(format, args) });
		}

		#endregion

		#region public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)

		/// <summary>
		/// Writes the given message to the underlying text file.
		/// </summary>
		/// <param name="eventCache">Detailed information about the event.</param>
		/// <param name="source">The source name of the event.</param>
		/// <param name="eventType">The type of the event.</param>
		/// <param name="id">The id of the event.</param>
		/// <param name="message">The message to write to the underlying text file.</param>
		public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)
		{
			if (null != this.Filter && !this.Filter.ShouldTrace(eventCache, source, eventType, id, message, null, null, null))
				return;

			this.TraceData(eventCache, source, eventType, id, new object[] { message });
		}

		#endregion

		#region public override void Write(string message)

		/// <summary>
		/// Writes the given message to the underlying text file.
		/// </summary>
		/// <param name="message">The message to write.</param>
		public override void Write(string message)
		{
			if (!string.IsNullOrEmpty(message))
			{
				lock (StreamLock)
				{
					using (StreamWriter writer = new StreamWriter(string.Format(@"{0}\{1}", this.OutputFolder, this.CreateFileName(null)), true))
					{
						writer.Write(message.Replace(Environment.NewLine, @"\n"));
					}
				}
			}
		}

		#endregion
		
		#region public override void WriteLine(string message)

		/// <summary>
		/// Writes the message and a line break to the underlying text file.
		/// </summary>
		/// <param name="message">The message to write.</param>
		public override void WriteLine(string message)
		{
			if (!string.IsNullOrEmpty(message))
			{
				lock (StreamLock)
				{
					using (StreamWriter writer = new StreamWriter(string.Format(@"{0}\{1}", this.OutputFolder, this.CreateFileName(null)), true))
					{
						writer.WriteLine(message.Replace(Environment.NewLine, @"\n"));
					}
				}
			}
		}

		#endregion



		#region protected virtual string CreateFileName(string source)

		/// <summary>
		/// When implemented in a class, creates the log file name to write the traced
		/// data to.
		/// </summary>
		/// <param name="source">The source of the event for which to create the file name. Note that this parameter can be null.</param>
		protected virtual string CreateFileName(string source)
		{
			return string.Format("{0:yyyyMMddHH}00.txt", DateTime.Now);
		}

		#endregion



		#region private void EnsureFolder(string filePath)

		/// <summary>
		/// Ensures that the folder of the given file exists.
		/// </summary>
		/// <param name="filePath">The full path to the file whose folder to check for existance.</param>
		private void EnsureFolder(string filePath)
		{
			FileInfo fi = new FileInfo(filePath);
			if (!fi.Directory.Exists)
				fi.Directory.Create();
		}

		#endregion
		
		#region private string FormatObject(object o)

		/// <summary>
		/// Formats the object to a string.
		/// </summary>
		private string FormatObject(object o)
		{
			if (null == o)
				return null;


			if (o is string)
			{
				return ((string)o).Replace("\t", "\\t").Replace("=", @"\u003d").Replace(Environment.NewLine, "\\n");
			}

			if (o is KeyValuePair<string, object>)
			{
				KeyValuePair<string, object> val = (KeyValuePair<string, object>)o;
				return string.Format("{0}={1}", this.FormatObject(val.Key), this.FormatObject(val.Value));
			}

			if (o is XmlNode)
			{
				return this.FormatObject(((XmlNode)o).OuterXml);
			}

			if (o is StackFrame)
			{
				StackFrame frame = (StackFrame)o;
				var method = frame.GetMethod();
				return string.Format("{0} in {1}, {2}", method, method.DeclaringType.FullName, method.DeclaringType.Assembly);
			}

			if (o is IFormattable)
			{
				return this.FormatObject(((IFormattable)o).ToString(null, null));
			}

			if (o is IConvertible)
			{
				return this.FormatObject(((IConvertible)o).ToString(null));
			}

			return this.FormatObject(o.ToString());
		}

		#endregion

		#region private void WriteLine(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data)

		/// <summary>
		/// Writes the given data as a line to the underlying text file.
		/// </summary>
		/// <param name="eventCache">Detailed information about the event.</param>
		/// <param name="source">The source name of the event.</param>
		/// <param name="eventType">The type of the event.</param>
		/// <param name="id">The id of the event.</param>
		/// <param name="data">The data to write to the underlying text file.</param>
		private void WriteLine(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data)
		{
			if (null != this.Filter && !this.Filter.ShouldTrace(eventCache, source, eventType, id, null, null, null, data))
				return;

			if (null == data)
				data = new object[0];

			List<object> dataList = new List<object>(data);

			StringBuilder sb = new StringBuilder();

			sb.Append(eventType);

			if ((this.TraceOutputOptions & TraceOptions.DateTime) > 0)
				sb.AppendFormat("\t{0:u}", eventCache.DateTime.ToUniversalTime());

			sb.AppendFormat("\t{0}", source);

			sb.AppendFormat("\tId={0}", id);

			if ((this.TraceOutputOptions & TraceOptions.LogicalOperationStack) > 0)
			{
				Converter<object, string> itemConverter = delegate(object item)
				{
					if (null != item)
					{
						return item.ToString();
					}
					return "";
				};

				string[] arr = Array.ConvertAll<object, string>(eventCache.LogicalOperationStack.ToArray(), itemConverter);
				sb.AppendFormat("\tLogicalOperationStack={0}", string.Join(",", arr));
			}

			if ((this.TraceOutputOptions & TraceOptions.Callstack) > 0)
			{
			StackTrace callstack = dataList.FirstOrDefault(o => o is StackTrace) as StackTrace;
				if (null != callstack)
				{
					sb.AppendFormat("\tCallstack={0}", callstack);
				}
				else
				{
					sb.AppendFormat("\tCallstack={0}", eventCache.Callstack);
				}
			}

			// Remove the call stack from the data, since if the configuration has specified that it will
			// be included, it was previously added to the output. If it has not been specified that is
			// should be in the output, it should be removed.
			dataList.RemoveAll(o => o is StackTrace);



			if ((this.TraceOutputOptions & TraceOptions.ProcessId) > 0)
				sb.AppendFormat("\tPID={0}", eventCache.ProcessId);

			if ((this.TraceOutputOptions & TraceOptions.ThreadId) > 0)
				sb.AppendFormat("\tThID={0}", eventCache.ThreadId);

			if ((this.TraceOutputOptions & TraceOptions.Timestamp) > 0)
				sb.AppendFormat("\tTimestamp={0}", eventCache.Timestamp);

			if (dataList.Count > 0)
			{
				sb.Append("\t");
				sb.Append(string.Join("\t", dataList.ConvertAll<string>(o => this.FormatObject(o)).ToArray()));
			}




			string path = Path.Combine(this.OutputFolder, this.CreateFileName(source));
			this.EnsureFolder(path);

			lock (StreamLock)
			{
				using (StreamWriter writer = new StreamWriter(path, true))
				{
					writer.WriteLine(sb.ToString().Replace(Environment.NewLine, @"\n"));
				}
			}

			this.WriteLine(sb.ToString());
		}

		#endregion

	}
}
